package cmd

import (
	"context"
	"errors"
	"fmt"
	"log"
	"os"
	"os/exec"
	"os/signal"
	"path/filepath"
	"syscall"
	"time"

	"github.com/sirupsen/logrus"
	"github.com/spf13/cobra"
	"github.com/xuelang-group/suanpan-appconnector/cmd/appconnector/global"
	"github.com/xuelang-group/suanpan-appconnector/pkg/common"
	"github.com/xuelang-group/suanpan-appconnector/pkg/config"
	"github.com/xuelang-group/suanpan-appconnector/pkg/file"
	"github.com/xuelang-group/suanpan-appconnector/pkg/graph"
	"github.com/xuelang-group/suanpan-appconnector/pkg/logkit"
	"github.com/xuelang-group/suanpan-appconnector/pkg/osslog"
	"github.com/xuelang-group/suanpan-appconnector/pkg/runcmd"
	"github.com/xuelang-group/suanpan-appconnector/pkg/state"
	"github.com/xuelang-group/suanpan-appconnector/pkg/storage"
	"github.com/xuelang-group/suanpan-appconnector/pkg/suanpan_log"
)

var rootCmd = &cobra.Command{
	Use:   "sp-connect",
	Short: "suanpan sp-connect go verson",
	Long: `A go version for connect suanpan
				  matianed by heng.lu`,
	RunE:    run,
	Version: global.PrintVersion(),
}

var cancelChildFunc context.CancelFunc

func Execute() {
	if err := rootCmd.Execute(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

func panicHandler(err error) {
	logrus.Error("connect panic with error, set node status and ready to close")
	entry := config.GetEnv().SpEntry
	switch entry {
	//case common.NORMAL_CONNECT_ENTRY:
	//	{
	//		if err := global.Pubsub.Publish(&pubsub.PublishRequest{
	//			PubsubName: "CONNECTOR-NODESTATUS",
	//			Topic:      common.GetEventsHubName(common.GetUserId(), common.GetAppId()),
	//			Data: map[string]interface{}{
	//				events.EventsSpecVersionKey: events.EventsSpecversionV1,
	//				events.EventsIdKey:          "id",
	//				events.EventsSourceKey:      common.GetNodeId(),
	//				events.EventsTypeKey:        events.EventsTypeNodeState,
	//				events.EventsReasonKey:      events.RunningStateChanged,

	//				"status": state.Failed,
	//			},
	//			Metadata: map[string]string{
	//				"AA": "BB",
	//			},
	//		}); err != nil {
	//			logrus.Errorf("pubsub nodestatus setting failed, %v", err)
	//		}
	//	}
	default:
		{
			msg := ""
			if entry == common.SH_SOLVER_HELPER {
				var logMsg string
				logPath, err := getLogPath()
				if err != nil {
					log.Println("get log path failed, ", err)
					logrus.Errorf("get log path failed, %v", err)
				}
				if err == nil {
					logMsg = osslog.ReadLogFileToString(logPath, 1024*1024*1) //default 1M
					msg += logMsg
				} else {
					logMsg = ""
				}
			}
			msg += "panic error: " + err.Error() + "\n"

			if err := state.SetRunningState(state.UpdateRunningStateReq{
				Method: state.MethodReqSetNodeStatus,
				Args: state.ArgsReq{
					NetworkId: common.GetAppId(),
					NodeId:    common.GetNodeId(),
					Status:    state.Failed,
				},
				Msg: msg,
			}); err != nil {
				logrus.Errorf("network status setting failed, %v", err)
			}
		}
	}
	if err != nil {
		logrus.Error("error detail:" + err.Error())
	}
}

func initLogKit(logkitLevel string) {
	var h logkit.LogKitHandler = logkit.LogKitHttpPostHandler
	logkit.RegisterHandler(h)
	hook := suanpan_log.NewSuanpanLogWithFunc("logkit", suanpan_log.GetSuanpanLoglevelByLogkitLevel(logkitLevel), true, logkit.LogkitFireFunc)
	logrus.AddHook(hook)
	logrus.WithField("logkit", true).Info("log with logkit active")
}

func getApiData(spEntry string) (*graph.ConnctApiResponseData, error) {
	var apiData *graph.ConnctApiResponseData
	switch spEntry {
	case common.SH_SOLVER_HELPER:
		{
			//inhired from cloudRunConnect.ts
			//and don't konw why machine learning need env SP_DEBUG
			os.Setenv("SP_DEBUG", "true")
			// connect
			apiData = graph.NewConnectApiResponseData()
			apiData.Data.DownloadLists = common.GetDownloadList()
			apiData.Data.WatchLists = common.GetWatchList()
			apiData.Data.UploadLists = common.GetUploadList()
			apiData.Data.Cmd = common.GetCmd()
		}
	default:
		return nil, fmt.Errorf("unknow entry: %s", spEntry)
	}
	return apiData, nil
}

func initOssLog() error {
	logPath, err := getLogPath()
	if err != nil {
		return err
	}
	pushToOssFlag := config.GetArgs().App.EnableOSSLog
	ossLogSize := config.GetArgs().App.OssLogSize
	ossLogBacks := config.GetArgs().App.OssLogBackups
	_, err = osslog.NewOssLogHelper(osslog.OSS_LOG_HELPER_DEFAULT_INTERVAL_SECOND, ossLogSize, ossLogBacks, common.GetOssLogRemotePath(),
		logPath, true, pushToOssFlag)
	if err != nil {
		return err
	}
	log.Println("oss log helper init ok...")
	return nil
}

func getLogPath() (string, error) {
	// just save to path /var/log/suanpan/appid-userid-nodeid/ to accept new-oss-log
	// the file will be conflict by nodeid (if node restart)
	// so use datetime+GUID-short for name and if has rotate juse use datetime+GUID-short.rfc3339datetime stop
	logDir := fmt.Sprintf("/var/log/suanpan/%s-%s-%s/", common.GetAppId(), common.GetUserId(), common.GetNodeId())
	if _, err := os.Stat(logDir); os.IsNotExist(err) {
		err = os.MkdirAll(filepath.Dir(logDir), 0755)
		if err != nil {
			return "", err
		}
	}
	logPath := logDir + config.GetEnv().K8SPodName
	return logPath, nil
}

func doRunCmdOnApiInfo(apiData *graph.ConnctApiResponseData, childProcessCh chan error) {
	if apiData == nil {
		return
	}
	switch config.GetEnv().SpEntry {
	//case common.NORMAL_CONNECT_ENTRY:
	//	{
	//		if err := global.Pubsub.Publish(&pubsub.PublishRequest{
	//			PubsubName: "CONNECTOR-NODESTATUS",
	//			Topic:      common.GetEventsHubName(common.GetUserId(), common.GetAppId()),
	//			Data: map[string]interface{}{
	//				events.EventsSpecVersionKey: events.EventsSpecversionV1,
	//				events.EventsIdKey:          "id",
	//				events.EventsSourceKey:      common.GetNodeId(),
	//				events.EventsTypeKey:        events.EventsTypeNodeState,
	//				events.EventsReasonKey:      events.RunningStateChanged,

	//				"status": state.Running,
	//			},
	//			Metadata: map[string]string{
	//				"AA": "BB",
	//			},
	//		}); err != nil {
	//			logrus.Errorf("pubsub nodestatus setting failed, %v", err)
	//		}
	//	}
	default:
		{
			if err := state.SetRunningState(state.UpdateRunningStateReq{
				Method: state.MethodReqSetNodeStatus,
				Args: state.ArgsReq{
					NetworkId: common.GetAppId(),
					NodeId:    common.GetNodeId(),
					Status:    state.Running,
				},
			}); err != nil {
				logrus.Errorf("network status setting failed, %v", err)
			}
		}
	}
	logrus.Infof("prepare to download asserts on %v", apiData.Data.DownloadLists)
	err := file.SyncFromOssStorage(&apiData.Data.DownloadLists)
	if err, ok := err.(*storage.StorageError); ok {
		common.PanicError(fmt.Errorf("oss Error painic: %w", err))
	}
	if err != nil {
		logrus.Error(err)
	}

	//logrus.Infof("prepare to watch asserts on %v", apiData.Data.WatchLists)
	//file.SetFileWatcher(&apiData.Data.WatchLists)
	ctx, cl := context.WithCancel(context.Background())
	cancelChildFunc = cl
	//oss log update
	logrus.Debugf("run cmd: %s, env:%s", apiData.Data.Cmd, apiData.Data.Env)
	err = runcmd.RunCmd(ctx, apiData.Data.Cmd, apiData.Data.Env, childProcessCh)
	if err != nil {
		common.PanicError(err)
	}

	//report status
	//if in normal mode success = success to run command and wait
	//if in machine_learning mode success = machine learning run complete and return 0 code
	//if config.GetEnv().SpEntry == common.NORMAL_CONNECT_ENTRY {
	//	err := global.Pubsub.Publish(&pubsub.PublishRequest{
	//		PubsubName: "CONNECTOR-NODESTATUS",
	//		Topic:      common.GetEventsHubName(common.GetUserId(), common.GetAppId()),
	//		Data: map[string]interface{}{
	//			events.EventsSpecVersionKey: events.EventsSpecversionV1,
	//			events.EventsIdKey:          "id",
	//			events.EventsSourceKey:      common.GetNodeId(),
	//			events.EventsTypeKey:        events.EventsTypeNodeState,
	//			events.EventsReasonKey:      events.RunningStateChanged,

	//			"status": state.Success,
	//		},
	//		Metadata: map[string]string{
	//			"AA": "BB",
	//		},
	//	})
	//	if err != nil {
	//		logrus.Errorf("pubsub nodestatus setting failed, %v", err)
	//	}
	//}
}

func start(spEntry string) {
	apiData, err := getApiData(spEntry)
	if err != nil {
		common.PanicError(err)
	}
	childProcessCh := make(chan error)
	doRunCmdOnApiInfo(apiData, childProcessCh)
	for procErr := range childProcessCh {
		logrus.Infof("get child process Info %s, try to restart", procErr.Error())
		if exiterr, ok := procErr.(*exec.ExitError); ok {
			if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
				exitstatus := status.ExitStatus()
				logrus.Infof("child process exit with code %v", exitstatus)
				// log .println will be print in logkit
				log.Println("child process exit with code %v", exitstatus)
				switch exitstatus {
				//no reload info
				//case runcmd.CONFIG_RELOAD:
				//	logrus.Infof("config reload")
				//	file.RemoveFileWatcher()

				//	if spEntry == common.NORMAL_CONNECT_ENTRY {
				//		apiData = graph.GetGraphInfo()
				//		if apiData == nil {
				//			common.PanicError(errors.New("api data not exist"))
				//		}
				//	}
				//	doRunCmdOnApiInfo(apiData, childProcessCh)
				default:
					common.PanicError(fmt.Errorf("unknow exit code:%v, painc", exitstatus))
				}
			} else {
				common.PanicError(procErr)
			}
		} else {
			common.PanicError(procErr)
		}
	}

	//normal exit  chan childProcessCh closed
	if spEntry == common.SH_SOLVER_HELPER {
		//upload result to oss
		env := config.GetEnv()
		appId := common.GetAppId()
		var remotePath string
		if appId != "" {
			remotePath = env.ResultOSSPath + "/" + appId
		} else {
			remotePath = env.ResultOSSPath + "/" + "noappiddata"
		}

		//upload result to oss env path
		if env.ResultPath != "" {
			log.Println("upload result to oss, localpath:", env.ResultPath, "ossPath:", remotePath)
			err = file.PushFilesToOss(env.ResultPath, remotePath)
			if err != nil {
				log.Println("upload result to oss failed, ", err)
				logrus.Errorf("upload result to oss failed, %v", err)
				common.PanicError(err)
			}
		}
		//upload result to oss by uploadList
		logrus.Infof("upload result to oss by uploadList: %v", apiData.Data.UploadLists)
		for _, upload := range apiData.Data.UploadLists {
			logrus.Infof("upload result to oss by uploadList: %v", upload)
			err = file.PushFilesToOss(upload.LocalPath, upload.OssPath)
			if err != nil {
				log.Println("upload result to oss failed, ", err)
				logrus.Errorf("upload result to oss failed, %v", err)
				common.PanicError(err)
			}
		}

		logPath, err := getLogPath()
		if err != nil {
			log.Println("get log path failed, ", err)
			logrus.Errorf("get log path failed, %v", err)
		}

		var logMsg string
		if err == nil {
			logMsg = osslog.ReadLogFileToString(logPath, 1024*1024*1) //default 1M
		} else {
			logMsg = ""
		}
		logrus.Info("machine-learning job run completed , exit connect_run")
		//logrus.Info("logMsg: ", logMsg)
		err = state.SetRunningState(state.UpdateRunningStateReq{
			Method: state.MethodReqSetNodeStatus,
			Args: state.ArgsReq{
				NetworkId: common.GetAppId(),
				NodeId:    common.GetNodeId(),
				Status:    state.Success,
			},
			Msg: logMsg,
		})
		if err != nil {
			logrus.Errorf("network status setting failed, %v", err)
		}

	} else {
		common.PanicError(errors.New("child process exit with code 0"))
	}
	time.Sleep(3 * time.Second) //sleep 1s to wate node status update finished
}

func run(cmd *cobra.Command, args []string) error {
	if len(args) == 0 {
		return errors.New("no args")
	}

	env := config.GetEnv()
	logrus.Infof("connect started with spentry: %s ", env.SpEntry)

	//add panic handler
	common.RegisterPanicFunction(panicHandler)

	if err := common.UpdateGeneralParamByBase64Str(args[0]); err != nil {
		common.PanicError(err)
	}

	if err := common.CheckArgsAvailable(); err != nil {
		common.PanicError(err)
	}

	//init logkit
	//logkitLevel := env.SpLogkitLogsLevel
	//initLogKit(logkitLevel)
	logrus.Info("connect run...")

	//handle quit signal
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	go func() {
		sig := <-quit
		logrus.Errorf("get signal:%s, ready to dispose connect", sig)

		if cancelChildFunc != nil {
			logrus.Errorf("cancel child process")
			cancelChildFunc()
		}

		//wait 3 second to wait nodestatus sent by async
		//wait 3 second to wait logkit sent by async
		time.Sleep(2 * time.Second)

		//exit without any defer
		fmt.Println("exit, bye..")
		os.Exit(0)
	}()

	if err := initOssLog(); err != nil {
		common.PanicError(err)
	}
	log.Println("connect started..")
	log.Println("Solver task id:", common.GetAppId())
	log.Println(global.PrintVersion())
	start(env.SpEntry)
	return nil
}
